/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007-2008 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.basics.manager.impl;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.Stack;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.PropertyException;

import com.genia.toolbox.basics.bean.Marshaller;
import com.genia.toolbox.basics.bean.Unmarshaller;
import com.genia.toolbox.basics.exception.technical.TechnicalException;
import com.genia.toolbox.basics.manager.ExceptionManager;
import com.genia.toolbox.basics.manager.XmlMappingManager;

/**
 * Implementation of {@link XmlMappingManager}.
 */
public class JaxbXmlMappingManagerImpl
    implements XmlMappingManager
{

  /**
   * {@link Unmarshaller} to use with betwixt.
   */
  private class JaxbUnmarshaller
      implements Unmarshaller
  {

    /**
     * Called by the garbage collector on an object when garbage collection
     * determines that there are no more references to the object. A subclass
     * overrides the <code>finalize</code> method to dispose of system
     * resources or to perform other cleanup.
     * 
     * @throws Throwable
     *           the <code>Exception</code> raised by this method
     * @see java.lang.Object#finalize()
     */
    @Override
    protected void finalize()
        throws Throwable
    {
      try {
        if (currentJaxbUnmarshaller != null && releaseJaxbUnmarshaller) {
          release(currentJaxbUnmarshaller);
        }
      }
      finally {
        super.finalize();
      }
    }

    /**
     * the current {@link  javax.xml.bind.Unmarshaller} to use.
     */
    private javax.xml.bind.Unmarshaller currentJaxbUnmarshaller = null;

    /**
     * whether it is safe to release the {@link javax.xml.bind.Unmarshaller} for
     * further use.
     */
    private boolean releaseJaxbUnmarshaller = true;



    /**
     * returns the current {@link javax.xml.bind.Unmarshaller} to use.
     * 
     * @return the current {@link javax.xml.bind.Unmarshaller} to use
     * @throws TechnicalException
     *           if an error occured while trying to retrieve a
     *           {@link javax.xml.bind.Unmarshaller}
     */
    private javax.xml.bind.Unmarshaller getCurrentUnmarshaller()
        throws TechnicalException
    {
      if (currentJaxbUnmarshaller == null) {
        currentJaxbUnmarshaller = getJaxbUnmarshaller();
      }
      return currentJaxbUnmarshaller;
    }



    /**
     * Unmarshal XML data from the specified file and return the resulting
     * content tree.
     * <p>
     * Implements <a href="#unmarshalGlobal">Unmarshal Global Root Element</a>.
     * 
     * @param file
     *          the file to unmarshal XML data from
     * @return the newly created root object of the java content tree
     * @throws TechnicalException
     *           If any unexpected errors occur while unmarshalling
     * @see com.genia.toolbox.basics.bean.Unmarshaller#unmarshal(java.io.File)
     */
    public Object unmarshal(File file)
        throws TechnicalException
    {
      try {
        releaseJaxbUnmarshaller = false;
        Object result = getCurrentUnmarshaller().unmarshal(file);
        releaseJaxbUnmarshaller = true;
        return result;
      }
      catch (Exception e) {
        throw getExceptionManager().convertException(e);
      }
    }



    /**
     * Unmarshal XML data from the specified InputStream and return the
     * resulting content tree. Validation event location information may be
     * incomplete when using this form of the unmarshal API.
     * <p>
     * Implements <a href="#unmarshalGlobal">Unmarshal Global Root Element</a>.
     * 
     * @param is
     *          the InputStream to unmarshal XML data from
     * @return the newly created root object of the java content tree
     * @throws TechnicalException
     *           If any unexpected errors occur while unmarshalling
     * @see com.genia.toolbox.basics.bean.Unmarshaller#unmarshal(java.io.InputStream)
     */
    public Object unmarshal(InputStream is)
        throws TechnicalException
    {
      try {
        releaseJaxbUnmarshaller = false;
        Object result = getCurrentUnmarshaller().unmarshal(is);
        releaseJaxbUnmarshaller = true;
        return result;
      }
      catch (Exception e) {
        throw getExceptionManager().convertException(e);
      }
    }



    /**
     * Unmarshal XML data from the specified Reader and return the resulting
     * content tree. Validation event location information may be incomplete
     * when using this form of the unmarshal API, because a Reader does not
     * provide the system ID.
     * <p>
     * Implements <a href="#unmarshalGlobal">Unmarshal Global Root Element</a>.
     * 
     * @param reader
     *          the Reader to unmarshal XML data from
     * @return the newly created root object of the java content tree
     * @throws TechnicalException
     *           If any unexpected errors occur while unmarshalling
     * @see com.genia.toolbox.basics.bean.Unmarshaller#unmarshal(java.io.Reader)
     */
    public Object unmarshal(Reader reader)
        throws TechnicalException
    {
      try {
        releaseJaxbUnmarshaller = false;
        Object result = getCurrentUnmarshaller().unmarshal(reader);
        releaseJaxbUnmarshaller = true;
        return result;
      }
      catch (Exception e) {
        throw getExceptionManager().convertException(e);
      }
    }
  }

  /**
   * {@link Marshaller} to use with Jaxb.
   */
  private class JaxbMarshaller
      implements Marshaller
  {

    /**
     * whether the xml produced is a fragment.
     */
    private final boolean xmlFragment;



    /**
     * constructor.
     * 
     * @param xmlFragment
     *          whether the xml produced is a fragment
     */
    public JaxbMarshaller(boolean xmlFragment)
    {
      this.xmlFragment = xmlFragment;
    }



    /**
     * Called by the garbage collector on an object when garbage collection
     * determines that there are no more references to the object. A subclass
     * overrides the <code>finalize</code> method to dispose of system
     * resources or to perform other cleanup.
     * 
     * @throws Throwable
     *           the <code>Exception</code> raised by this method
     * @see java.lang.Object#finalize()
     */
    @Override
    protected void finalize()
        throws Throwable
    {
      try {
        if (currentJaxbMarshaller != null && releaseJaxbMarshaller) {
          release(currentJaxbMarshaller);
        }
      }
      finally {
        super.finalize();
      }
    }

    /**
     * the current {@link  javax.xml.bind.Marshaller} to use.
     */
    private javax.xml.bind.Marshaller currentJaxbMarshaller = null;

    /**
     * whether it is safe to release the {@link javax.xml.bind.Marshaller} for
     * further use.
     */
    private boolean releaseJaxbMarshaller = true;



    /**
     * returns the current {@link javax.xml.bind.Marshaller} to use.
     * 
     * @return the current {@link javax.xml.bind.Marshaller} to use
     * @throws TechnicalException
     *           if an error occured while trying to retrieve a
     *           {@link javax.xml.bind.Marshaller}
     */
    private javax.xml.bind.Marshaller getCurrentMarshaller()
        throws TechnicalException
    {
      if (currentJaxbMarshaller == null) {
        currentJaxbMarshaller = getJaxbMarshaller();
        if (xmlFragment) {
          try {
            currentJaxbMarshaller.setProperty(javax.xml.bind.Marshaller.JAXB_FRAGMENT, true);
          }
          catch (PropertyException e) {
            throw getExceptionManager().convertException(e);
          }
        }
      }
      return currentJaxbMarshaller;
    }



    /**
     * Marshal the content tree rooted at <tt>jaxbElement</tt> into an output
     * stream.
     * 
     * @param object
     *          The root of content tree to be marshalled.
     * @param os
     *          XML will be added to this stream.
     * @throws TechnicalException
     *           If any unexpected problem occurs during the marshalling.
     * @see com.genia.toolbox.basics.bean.Marshaller#marshal(java.lang.Object,
     *      java.io.OutputStream)
     */
    public void marshal(Object object, OutputStream os)
        throws TechnicalException
    {
      try {
        releaseJaxbMarshaller = false;
        getCurrentMarshaller().marshal(object, os);
        releaseJaxbMarshaller = true;
      }
      catch (Exception e) {
        throw getExceptionManager().convertException(e);
      }
    }



    /**
     * Marshal the content tree rooted at <tt>jaxbElement</tt> into a file.
     * 
     * @param object
     *          The root of content tree to be marshalled.
     * @param output
     *          File to be written. If this file already exists, it will be
     *          overwritten.
     * @throws TechnicalException
     *           If any unexpected problem occurs during the marshalling.
     * @see com.genia.toolbox.basics.bean.Marshaller#marshal(java.lang.Object,
     *      java.io.File)
     */
    public void marshal(Object object, File output)
        throws TechnicalException
    {
      try {
        marshal(object, new FileOutputStream(output));
      }
      catch (IOException e) {
        throw getExceptionManager().convertException(e);
      }
    }



    /**
     * Marshal the content tree rooted at <tt>jaxbElement</tt> into a Writer.
     * 
     * @param object
     *          The root of content tree to be marshalled.
     * @param writer
     *          XML will be sent to this writer.
     * @throws TechnicalException
     *           If any unexpected problem occurs during the marshalling.
     * @see com.genia.toolbox.basics.bean.Marshaller#marshal(java.lang.Object,
     *      java.io.Writer)
     */
    public void marshal(Object object, Writer writer)
        throws TechnicalException
    {
      try {
        releaseJaxbMarshaller = false;
        getCurrentMarshaller().marshal(object, writer);
        releaseJaxbMarshaller = true;
      }
      catch (Exception e) {
        throw getExceptionManager().convertException(e);
      }
    }

  }

  /**
   * the base package of the jaxb enabled beans.
   */
  private String basePackage;

  /**
   * {@link Stack} of usable {@link javax.xml.bind.Marshaller}.
   */
  private final Stack<javax.xml.bind.Marshaller> marshallers = new Stack<javax.xml.bind.Marshaller>();

  /**
   * {@link Stack} of usable {@link javax.xml.bind.Unmarshaller}.
   */
  private final Stack<javax.xml.bind.Unmarshaller> unmarshallers = new Stack<javax.xml.bind.Unmarshaller>();

  /**
   * the {@link ExceptionManager} to use.
   */
  private ExceptionManager exceptionManager;

  /**
   * maximum number of (un)marshallers to keep.
   */
  private int maxCached = DEFAULT_MAX_CACHED;

  /**
   * default maximum number of (un)marshaller to keep.
   */
  private static final int DEFAULT_MAX_CACHED = 5;

  /**
   * the {@link JAXBContext} to use.
   */
  private JAXBContext jaxbContext = null;



  /**
   * returns the {@link javax.xml.bind.Marshaller} to parse xml files.
   * 
   * @return the {@link javax.xml.bind.Marshaller} to parse xml files
   * @throws TechnicalException
   *           if an error occured
   * @see com.genia.toolbox.basics.manager.XmlMappingManager#getBeanReader()
   */
  private javax.xml.bind.Marshaller getJaxbMarshaller()
      throws TechnicalException
  {
    synchronized (this) {

      javax.xml.bind.Marshaller marshaller = marshallers.isEmpty() ? null : marshallers.pop();
      if (marshaller == null) {
        try {
          marshaller = getJaxbContext().createMarshaller();
        }
        catch (JAXBException e) {
          throw getExceptionManager().convertException(e);
        }
      }
      return marshaller;
    }
  }



  /**
   * release an unused {@link javax.xml.bind.Marshaller}.
   * 
   * @param marshaller
   *          the {@link javax.xml.bind.Marshaller} to release
   */
  private void release(javax.xml.bind.Marshaller marshaller)
  {
    if (marshallers.size() < getMaxCached()) {
      marshallers.push(marshaller);
    }
  }



  /**
   * returns the {@link javax.xml.bind.Unmarshaller} to parse xml files.
   * 
   * @return the {@link javax.xml.bind.Unmarshaller} to parse xml files
   * @throws TechnicalException
   *           if an error occured
   * @see com.genia.toolbox.basics.manager.XmlMappingManager#getBeanReader()
   */
  private javax.xml.bind.Unmarshaller getJaxbUnmarshaller()
      throws TechnicalException
  {
    synchronized (this) {

      javax.xml.bind.Unmarshaller unmarshaller = unmarshallers.isEmpty() ? null : unmarshallers.pop();
      if (unmarshaller == null) {
        try {
          unmarshaller = getJaxbContext().createUnmarshaller();
        }
        catch (JAXBException e) {
          throw getExceptionManager().convertException(e);
        }
      }
      return unmarshaller;
    }
  }



  /**
   * release an unused {@link javax.xml.bind.Unmarshaller}.
   * 
   * @param unmarshaller
   *          the {@link javax.xml.bind.Unmarshaller} to release
   */
  private void release(javax.xml.bind.Unmarshaller unmarshaller)
  {
    if (unmarshallers.size() < getMaxCached()) {
      unmarshallers.push(unmarshaller);
    }
  }



  /**
   * returns a {@link Marshaller} that allows to transform bean to xml.
   * 
   * @return a {@link Marshaller} that allows to transform bean to xml
   * @see com.genia.toolbox.basics.manager.XmlMappingManager#getMarshaller()
   */
  public Marshaller getMarshaller()
  {
    return getMarshaller(false);
  }



  /**
   * returns an {@link Unmarshaller} that allows to print a xml representation
   * of a bean.
   * 
   * @return an {@link Unmarshaller} that allows to print a xml representation
   *         of a bean
   * @see com.genia.toolbox.basics.manager.XmlMappingManager#getUnmarshaller()
   */
  public Unmarshaller getUnmarshaller()
  {
    return new JaxbUnmarshaller();
  }



  /**
   * returns an {@link Unmarshaller} that allows to print a xml representation
   * of a bean.
   * 
   * @param xmlFragment
   *          whether the generated xml is a fragment
   * @return an {@link Unmarshaller} that allows to print a xml representation
   *         of a bean
   * @see com.genia.toolbox.basics.manager.XmlMappingManager#getMarshaller(boolean)
   */
  public Marshaller getMarshaller(boolean xmlFragment)
  {
    return new JaxbMarshaller(xmlFragment);
  }



  /**
   * getter for the basePackage property.
   * 
   * @return the basePackage
   */
  public String getBasePackage()
  {
    return basePackage;
  }



  /**
   * setter for the basePackage property.
   * 
   * @param basePackage
   *          the basePackage to set
   */
  public void setBasePackage(String basePackage)
  {
    this.basePackage = basePackage;
  }



  /**
   * getter for the exceptionManager property.
   * 
   * @return the exceptionManager
   */
  public ExceptionManager getExceptionManager()
  {
    return exceptionManager;
  }



  /**
   * setter for the exceptionManager property.
   * 
   * @param exceptionManager
   *          the exceptionManager to set
   */
  public void setExceptionManager(ExceptionManager exceptionManager)
  {
    this.exceptionManager = exceptionManager;
  }



  /**
   * getter for the maxCached property.
   * 
   * @return the maxCached
   */
  public int getMaxCached()
  {
    return maxCached;
  }



  /**
   * setter for the maxCached property.
   * 
   * @param maxCached
   *          the maxCached to set
   */
  public void setMaxCached(int maxCached)
  {
    this.maxCached = maxCached;
  }



  /**
   * getter for the jaxbContext property.
   * 
   * @return the jaxbContext
   * @throws TechnicalException
   *           if an error occured
   */
  public JAXBContext getJaxbContext()
      throws TechnicalException
  {
    if (jaxbContext == null) {
      try {
        jaxbContext = JAXBContext.newInstance(getBasePackage());
      }
      catch (JAXBException e) {
        throw getExceptionManager().convertException(e);
      }
    }
    return jaxbContext;
  }

}
